/*           Copyright(c) 2005-2008 Dave Sexton            *
 *                   HtmlEditor Control                    *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Xml;
using DaveSexton.Controls.Resources;

namespace DaveSexton.Controls
{
	partial class HtmlToXmlConverter
	{
		internal sealed class TagParser : HtmlParserBase
		{
			#region Public Properties
			public TagParser Parent { get { return parent; } }

			public bool IsSingleTagElement { get { return isSingleTagElement; } }

			public string Name { get { return name; } }

			/// <summary>
			/// When <see langword="true"/>, the parser will only read the tag's attributes and end tag, but will not write them to the underlying writer; 
			/// i.e., a caller can assign this property to remove particular elements from the document.  Child element's are unaffected and will be written
			/// to the element's parent unless they are explicitly skipped as well.
			/// </summary>
			public bool Skip { get; set; }
			#endregion

			#region Private / Protected
			private bool isSingleTagElement;
			private Dictionary<string, string> offsetAttributes;
			private readonly string name;
			private readonly TagParser parent;
			private TagParser offsetStartWritten;
			private readonly StringBuilder attributes = new StringBuilder(0);
			private readonly XmlNamespaceManager namespaces;
			private readonly bool removeTagPrefixes;
			private readonly XmlNameProcessor attributeNameProcessor;
			#endregion

			#region Constructors
			/// <summary>
			/// Constructs a new instance of the <see cref="TagParser" /> class.
			/// </summary>
			public TagParser(TextReader reader, XmlWriter writer, string name, XmlNameProcessor attributeNameProcessor, TagParser parent, bool removeTagPrefixes, XmlNamespaceManager namespaces, 
				Dictionary<string, Stack<HtmlToXmlConverter.TagParser>> allowableOffsetTagNames, List<string> startElementOnlyTagNames, List<string> booleanAttributeNames)
				: base(reader, writer, allowableOffsetTagNames, startElementOnlyTagNames, booleanAttributeNames)
			{
				this.name = name;
				this.parent = parent;
				this.namespaces = namespaces;
				this.removeTagPrefixes = removeTagPrefixes;
				this.attributeNameProcessor = attributeNameProcessor;
			}
			#endregion

			#region Methods
			public void ParseStartElement()
			{
				if (!Skip)
					WriteStartElement();

				XmlNodeType type;

				AttributeReader attribute = new AttributeReader(reader, writer, booleanAttributes);

				while ((type = MoveToNextAttribute()) == XmlNodeType.Attribute)
				{
					attribute.Read(attributeNameProcessor);

					if (!Skip)
					{
						string attributeName = attribute.Name;

						if (attributeName != null)
						{
							string value = attribute.Value;

							attributeName = WriteAttribute(attributeName, value);

							attributes.AppendFormat(" {0}=\"{1}\"", attributeName, value);

							if (offsetAttributes != null)
								offsetAttributes.Add(attributeName, value);
						}
					}
				}

				if (!Skip)
					WriteEndOfStartElement(type);
				else if (type == XmlNodeType.EndElement)
					isSingleTagElement = true;

				SkipNext();		// skip tag name end character
			}

			private void WriteStartElement()
			{
				WriteStartElement(name);

				if (offsetTags.ContainsKey(name))
				// found new offset tag
				{
					offsetAttributes = new Dictionary<string, string>(8);
					offsetTags[name].Push(this);
				}
			}

			private void WriteStartElement(string elementName)
			{
				int prefixIndex = elementName.IndexOf(':');

				if (prefixIndex > 0 && prefixIndex < elementName.Length - 1)
				{
					string prefix = elementName.Substring(0, prefixIndex);
					string localName = elementName.Substring(prefixIndex + 1);

					if (removeTagPrefixes)
						writer.WriteStartElement(localName);
					else
						writer.WriteStartElement(prefix, elementName.Substring(prefixIndex + 1), namespaces.LookupNamespace(prefix));
				}
				else
					writer.WriteStartElement(elementName);
			}

			private void WriteEndOfStartElement(XmlNodeType type)
			{
				if (type == XmlNodeType.EndElement || singleTags.Contains(name))
				{
					writer.WriteEndElement();
					isSingleTagElement = true;
				}
				else
					WriteOffsetStartElements();
			}

			private void WriteOffsetStartElements()
			{
				if (Skip)
					return;

				foreach (KeyValuePair<string, Stack<TagParser>> offsets in offsetTags)
				{
					foreach (TagParser offsetStart in new Stack<TagParser>(offsets.Value))		// reverse stack
					// TODO: possibly improve peformance by creating a reversible stack class (custom enumerator is needed)
					{
						if (offsetStart != this && offsetStart.offsetStartWritten == null && !offsetStart.Contains(this))
						{
							WriteStartElement(offsetStart.name);

							offsetStart.offsetStartWritten = this;

							foreach (KeyValuePair<string, string> attribute in offsetStart.offsetAttributes)
								WriteAttribute(attribute.Key, attribute.Value);
						}
					}
				}
			}

			private string WriteAttribute(string attributeName, string value)
			{
				int prefixIndex = attributeName.IndexOf(':');

				if (prefixIndex > 0 && prefixIndex < attributeName.Length - 1)
				{
					string prefix = attributeName.Substring(0, prefixIndex);
					string localName = attributeName.Substring(prefixIndex + 1);

					if (string.Equals(prefix, "xmlns", StringComparison.Ordinal))
					{
						namespaces.PushScope();
						namespaces.AddNamespace(localName, value);
					}
					else if (string.Equals(localName, "xmlns", StringComparison.Ordinal))
					{
						namespaces.PushScope();
						namespaces.AddNamespace("", value);
					}
					else if (removeTagPrefixes)
					{
						attributeName = localName;
						writer.WriteAttributeString(localName, value);
					}
					else
						writer.WriteAttributeString(prefix, localName, namespaces.LookupNamespace(prefix), value);
				}
				else if (string.Equals(attributeName, "xmlns", StringComparison.Ordinal))
				{
					namespaces.PushScope();
					namespaces.AddNamespace("", value);
				}
				else
					writer.WriteAttributeString(attributeName, value);

				return attributeName;
			}

			private void WriteOffsetEndElements()
			{
				if (Skip)
					return;

				foreach (KeyValuePair<string, Stack<TagParser>> offsets in offsetTags)
				{
					foreach (TagParser offsetStart in offsets.Value)
					{
						if (offsetStart.offsetStartWritten == this)
						{
							writer.WriteEndElement();
							offsetStart.offsetStartWritten = null;
						}
					}
				}
			}

			public bool Contains(TagParser parser)
			{
				while ((parser = parser.parent) != null)
				{
					if (this == parser)
						return true;
				}

				return false;
			}

      [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly")]
			private XmlNodeType MoveToNextAttribute()
			{
				SkipToContent(true, false);

				char c = PeekNextCharacter();

				if (c == ElementEnd)
				// single-tag element
				{
					reader.Read();		// skip end tag character

					if (PeekNextCharacter() != TagEnd)
						throw new ArgumentException(string.Format(System.Globalization.CultureInfo.CurrentCulture,
							Errors.HtmlToXmlConverterInvalidCharacterInTag, '/', name), "html");

					return XmlNodeType.EndElement;
				}
				else if (c == TagEnd)
					return XmlNodeType.Element;

				return XmlNodeType.Attribute;
			}

			public void WriteEndElement()
			{
				if (!Skip)
				{
					WriteOffsetEndElements();

					writer.WriteEndElement();
				}

				SkipTo(true, false, TagEnd);

				SkipNext();		// skip tag name end character
			}

			public void CreateEndElement()
			{
				if (!Skip && !isSingleTagElement)
					writer.WriteEndElement();
			}

			public override string ToString()
			{
				return "<" + (name ?? "?") + 
					((attributes.Length > 0) ? attributes.ToString() : string.Empty) + 
					((isSingleTagElement) ? " />" : ">");
			}
			#endregion
		}
	}
}
